So far, we've concentrated on self-contained applications that haven't come in contact with the outside world. But on many occasions, you'll need your application to interact with its environment, including other applications that run in parallel with yours. This section introduces the topic and describes some techniques for managing such interactions.
The App object is provided by the Visual Basic library and represents the application being executed. The App object exposes a lot of properties and methods, many of which are somewhat advanced and will be explained later in the book.
The EXEName and the Path properties return the name and the path of the executable file (if running as a stand-alone EXE file) or the project name (if running inside the environment). These properties are often used together—for example, to locate an INI file that's stored in the same directory as the executable and that has the same base name:
IniFile = App.Path & IIf(Right$(App.Path, 1) <> "\", "\", "") _ & App.EXEName & ".INI" Open IniFile For Input As #1 ' and so on. |
Another common use for the App.Path property is to set the current directory to match the directory of the application so that all its ancillary files can be found without your having to specify their complete path:
' Let the application's directory be the current directory. On Error Resume Next ChDrive App.Path: ChDir App.Path |
CAUTION
The preceding snippet of code might fail under some conditions, in particular when the Visual Basic application is started from a remote network server. This happens because the App.Path property could return a UNC path (for example, \\servername\dirname\...) and the ChDrive command is unable to deal with such paths. For this reason, you should protect this code against unanticipated errors, and you should always provide your users with alternative ways to make the application point to its own directory (for example, by setting a key in the system Registry).
The PrevInstance property lets you determine whether there's another (compiled) instance of the application running in the system. This can be useful if you want to prevent the user from accidentally running two instances of your program:
Private Sub Form_Load() If App.PrevInstance Then ' Another instance of this application is running. Dim saveCaption As String saveCaption = Caption ' Modify this form's caption so that it isn't traced by ' the AppActivate command. Caption = Caption & Space$(5) On Error Resume Next AppActivate saveCaption ' Restore the Caption, in case AppActivate failed. Caption = saveCaption If Err = 0 Then Unload Me End If End Sub |
A couple of properties can be both read and modified at run time. The TaskVisible Boolean property determines whether the application is visible in the task list. The Title property is the string that identifies the application in the Windows task list. Its initial value is the string you enter at design time in the Make tab of the Project Properties dialog box.
Other properties of the App object return values that you entered at design time in the General and Make tabs of the Project Properties dialog box. (See Figure 5-2.) For example, the HelpFile property is the name of the associated help file, if you have any. The UnattendedApp and the RetainedProject properties report the state of the corresponding check boxes on the General tab of the dialog box (but their meaning will be made clear in Chapters 16 and 20, respectively). Taken together, the Major, Minor, and Revision properties return information about the version of the running executable. The Comments, CompanyName, FileDescription, LegalCopyright, LegalTrademarks, and ProductName properties let you query at run time other values that have been entered in the Make tab of the Project Properties dialog box. They're useful mostly when you're creating informative About Box dialog boxes or splash screens.
Figure 5-2. The General and Make tabs of the Project Properties dialog box.
In the 32-bit world of Windows 9x and Windows NT, exchanging information with other applications through the system clipboard might seem a bit old-fashioned, but it is a fact that the clipboard remains one of the simplest and most effective ways for end users to quickly copy data among applications. Visual Basic lets you control the system clipboard using the Clipboard global object. Compared with other Visual Basic objects, this is a very simple one in that it exposes only six methods and no properties.
To place a piece of text in the clipboard, you use the SetText method:
Clipboard.SetText Text, [Format] |
where format can be 1-vbCFText (plain text, the default), &HBF01-vbCFRTF (text in RTF format), or &HBF00-vbCFLink (DDE conversation information). This argument is necessary because the clipboard can store pieces of information in multiple formats. For example, if you have a RichTextBox control (a Microsoft ActiveX control described in Chapter 12), you can store the selected text in either vbCFText or vbCFRTF format and let the user paste your text in whatever format fits the target control.
Clipboard.Clear Clipboard.SetText RichTextBox1.SelText ' vbCFText is the default. Clipboard.SetText RichTextBox1.SelRTF, vbCFRTF |
CAUTION
In some circumstances and with some external applications, placing text on the clipboard doesn't work correctly unless you first reset the Clipboard object using its Clear method, as shown in the preceding code snippet.
You retrieve the text currently in the clipboard using the GetText method. You can specify which format you want to retrieve using the following syntax:
' For a regular TextBox control Text1.SelText = Clipboard.GetText() ' You can omit vbCFText. ' For a RichTextBox control RichTextBox1.SelRTF = Clipboard.GetText(vbCFRTF) |
In general, you don't know whether the clipboard actually includes text in RTF format, so you should test its current contents using the GetFormat method, which takes a format as an argument and returns a Boolean value that indicates whether the clipboard format matches the format parameter:
If Clipboard.GetFormat(vbCFRTF) Then ' The Clipboard contains data in RTF format. End If |
The value of format can be 1-vbCFText (plain text), 2-vbCFBitmap (bitmap), 3-vbCFMetafile (metafile), 8-vbCFDIB (Device Independent Bitmap), 9-vbCFPalette (color palette), &HBF01-vbCFRTF (text in RTF format), or &HBF00-vbCFLink (DDE conversation information). This is the correct sequence for pasting text into a RichTextBox control:
If Clipboard.GetFormat(vbCFRTF) Then RichTextBox1.SelRTF = Clipboard.GetText(vbCFRTF) ElseIf Clipboard.GetFormat(vbCFText) Then RichTextBox1.SelText = Clipboard.GetText() End If |
When you work with PictureBox and Image controls, you can retrieve an image stored in the Clipboard using the GetData method, which also requires a format attribute (vbCFBitmap, vbCFMetafile, vbCFDIB, or vbCFPalette—although with Image controls, you can use only vbCFBitmap). The correct sequence is
Dim frmt As Variant For Each frmt In Array(vbCFBitmap, vbCFMetafile, _ vbCFDIB, vbCFPalette) If Clipboard.GetFormat(frmt) Then Set Picture1.Picture = Clipboard.GetData(frmt) Exit For End If Next |
You can copy the current contents of a PictureBox or an Image control to the clipboard using the SetData method:
Clipboard.SetData Picture1.Picture ' You can also load an image from disk onto the clipboard. Clipboard.SetData LoadPicture("c:\myimage.bmp") |
In many Windows applications, all the clipboard commands are typically gathered in the Edit menu. The commands available to the user (and how your code processes them) depends on which control is the active control. Here you have two problems to solve: For a really user-friendly interface, you should disable all the menu items that don't apply to the active control and the current contents of the clipboard, and you must devise a cut-copy-paste strategy that works well in all situations.
When you have multiple controls on your forms, things become confusing quickly because you have to account for several potential problems. I have prepared a simple but complete demonstration program. (See Figure 5-3.) To let you easily reuse its code in your applications, all the references to controls are done through the form's ActiveControl property. Instead of testing the control type using a TypeOf or TypeName keyword, the code indirectly tests which properties are actually supported using the On Error Resume Next statement. (See the code in boldface in the following listing.) This approach lets you deal with any type of control, including third-party ActiveX controls, without having to modify the code when you add a new control to your Toolbox.
Figure 5-3. The Clipbord.vbp demonstration project shows how you can create a generic Edit menu that works with TextBox, RTF TextBox, and PictureBox controls.
' Items in Edit menu belong to a control array. These are their indices. Const MNU_EDITCUT = 2, MNU_EDITCOPY = 3 Const MNU_EDITPASTE = 4, MNU_EDITCLEAR = 6, MNU_EDITSELECTALL = 7 ' Enable/disable items in the Edit menu. Private Sub mnuEdit_Click() Dim supSelText As Boolean, supPicture As Boolean ' Check which properties are supported by the active control. On Error Resume Next ' These expressions return False only if the property isn't supported. supSelText = Len(ActiveControl.SelText) Or True supPicture = (ActiveControl.Picture Is Nothing) Or True If supSelText Then mnuEditItem(MNU_EDITCUT).Enabled = Len(ActiveControl.SelText) mnuEditItem(MNU_EDITPASTE).Enabled = Clipboard.GetFormat(vbCFText) mnuEditItem(MNU_EDITCLEAR).Enabled = Len(ActiveControl.SelText) mnuEditItem(MNU_EDITSELECTALL).Enabled = Len(ActiveControl.Text) ElseIf supPicture Then mnuEditItem(MNU_EDITCUT).Enabled = Not (ActiveControl.Picture _ Is Nothing) mnuEditItem(MNU_EDITPASTE).Enabled = Clipboard.GetFormat( _ vbCFBitmap) Or Clipboard.GetFormat(vbCFMetafile) mnuEditItem(MNU_EDITCLEAR).Enabled = _ Not (ActiveControl.Picture Is Nothing) Else ' Neither a text- nor a picture-based control mnuEditItem(MNU_EDITCUT).Enabled = False mnuEditItem(MNU_EDITPASTE).Enabled = False mnuEditItem(MNU_EDITCLEAR).Enabled = False mnuEditItem(MNU_EDITSELECTALL).Enabled = False End If ' The Copy menu command always has the same state as the Cut command. mnuEditItem(MNU_EDITCOPY).Enabled = mnuEditItem(MNU_EDITCUT).Enabled End Sub ' Actually perform copy-cut-paste commands. Private Sub mnuEditItem_Click(Index As Integer) Dim supSelText As Boolean, supSelRTF As Boolean, supPicture As Boolean ' Check which properties are supported by the active control. On Error Resume Next supSelText = Len(ActiveControl.SelText) >= 0 supSelRTF = Len(ActiveControl.SelRTF) >= 0 supPicture = (ActiveControl.Picture Is Nothing) Or True Err.Clear Select Case Index Case MNU_EDITCUT If supSelRTF Then Clipboard.Clear Clipboard.SetText ActiveControl.SelRTF, vbCFRTF ActiveControl.SelRTF = "" ElseIf supSelText Then Clipboard.Clear Clipboard.SetText ActiveControl.SelText ActiveControl.SelText = "" Else Clipboard.SetData ActiveControl.Picture Set ActiveControl.Picture = Nothing End If Case MNU_EDITCOPY ' Similar to Cut, but the current selection isn't deleted. If supSelRTF Then Clipboard.Clear Clipboard.SetText ActiveControl.SelRTF, vbCFRTF ElseIf supSelText Then Clipboard.Clear Clipboard.SetText ActiveControl.SelText Else Clipboard.SetData ActiveControl.Picture End If Case MNU_EDITPASTE If supSelRTF And Clipboard.GetFormat(vbCFRTF) Then ' Paste RTF text if possible. ActiveControl.SelRTF = Clipboard.GetText(vbCFText) ElseIf supSelText Then ' Else, paste regular text. ActiveControl.SelText = Clipboard.GetText(vbCFText) ElseIf Clipboard.GetFormat(vbCFBitmap) Then ' First, try with bitmap data. Set ActiveControl.Picture = _ Clipboard.GetData(vbCFBitmap) Else ' Else, try with metafile data. Set ActiveControl.Picture = _ Clipboard.GetData(vbCFMetafile) End If Case MNU_EDITCLEAR If supSelText Then ActiveControl.SelText = "" Else Set ActiveControl.Picture = Nothing End If Case MNU_EDITSELECTALL If supSelText Then ActiveControl.SelStart = 0 ActiveControl.SelLength = Len(ActiveControl.Text) End If End Select End Sub |
Many applications need to deliver their results on paper. Visual Basic provides you with a Printer object that exposes a number of properties and methods to finely control the appearance of your printer documents.
The Visual Basic library also exposes a Printers collection, which lets you collect information about all the printers installed on your system. Each item of this collection is a Printer object, and all its properties are read-only. In other words, you can read the characteristics of all the installed printers, but you can't modify them directly. If you want to modify a characteristic of a printer, you must first assign the item from the collection that represents your chosen printer to the Printer object and then change its properties.
The Printer object exposes many properties that allow you to determine the characteristics of an available printer and its driver. For example, the DeviceName property returns the name of the printer as it appears in the Control Panel, and the DriverName returns the name of the driver used by that peripheral. It's simple to fill a ListBox or a ComboBox control with this information:
For i = 0 To Printers.Count _ 1 cboPrinters.AddItem Printers(i).DeviceName & " [" & _ Printers(i).DriverName & "]" Next |
The Port property returns the port the printer is connected to (for example, LPT1:). The ColorMode property determines whether the printer can print in color. (It can be 1-vbPRCMMonochrome or 2-vbPRCMColor.) The Orientation property reflects the current orientation of the page. (It can be 1-vbPRORPortrait, 2-vbPRORLandscape.) The PrinterQuality property returns the current resolution. (It can be 1-vbPRPQDraft, 2-vbPRPQLow, 3-vbPRPQMedium, or 4-vbPRPQHigh.)
Other properties include PaperSize (the size of the paper), PaperBin (the paper bin the paper is fed from), Duplex (the ability to print both sides of a sheet of paper), Copies (the number of copies to be printed), and Zoom (the zoom factor applied when printing). For more information about these properties, see the Visual Basic documentation. On the companion CD, you'll find a demonstration program (shown in Figure 5-4) that lets you enumerate all the printers in your system, browse their properties, and print a page on each of them.
Figure 5-4. Run this demonstration program to see the Printers collection and the Printer object in action.
A modern application should give its users the ability to work with any printer among those installed on the system. In Visual Basic, you do this by assigning an element of the Printers collection that describes your chosen printer to the Printer object. For example, if you've filled a ComboBox control with the names of all installed printers, you can let users select one of them by clicking on a Make Current button:
Private Sub cmdMakeCurrent_Click() Set Printer = Printers(cboPrinters.ListIndex) End Sub |
In contrast to the restrictions you must observe for Printer objects stored in the Printers collection, whose properties are read-only, you can modify the properties of the Printer object. Theoretically, all the properties seen so far can be written to, with the only exceptions being DeviceName, DriverName, and Port. In practice, however, what happens when you assign a value to a property depends on the printer and the driver. For example, if the current printer is monochrome it doesn't make any sense to assign the 2-vbPRCMColor value to the ColorMode property. This assignment either can be ignored or it can raise an error, depending on the driver in use. In general, if a property isn't supported, it returns 0.
At times, you might need to understand which item in the Printers collection the Printer object corresponds to, for example, when you want to print temporarily using another printer and then restore the original printer. You can do this by comparing the DeviceName property of the Printer object with the value returned by each item in the Printers collection:
' Determine the index of the Printer object in the Printers collection. For i = 0 To Printers.Count _ 1 If Printer.DeviceName = Printers(i).DeviceName Then PrinterIndex = i: Exit For End If Next ' Prepare to output to the printer selected by the user. Set Printer = Printers(cboPrinters.ListIndex) ' ... ' Restore the original printer. Set Printer = Printers(PrinterIndex) |
Another way to let users print to the printer of their choice is to set the Printer's TrackDefault property to True. When you do that, the Printer object automatically refers to the printer selected in the Control Panel.
Sending output to the Printer object is trivial because this object supports all the graphic methods that are exposed by the Form and the PictureBox objects, including Print, PSet, Line, Circle, and PaintPicture. You can also control the appearance of the output using standard properties such as the Font object and the individual Fontxxxx properties, the CurrentX and CurrentY properties, and the ForeColor property.
Three methods are peculiar to the Printer object. The EndDoc method informs the Printer object that all the data has been sent and that the actual printing operation can start. The KillDoc method terminates the current print job before sending anything to the printer device. Finally the NewPage method sends the current page to the printer (or the print spooler) and advances to the next page. It also resets the printing position at the upper left corner of the printable area in the page and increments the page number. The current page number can be retrieved using the Page property. Here's an example that prints a two-page document:
Printer.Print "Page One" Printer.NewPage Printer.Print "Page Two" Printer.EndDoc |
The Printer object also supports the standard properties ScaleLeft, ScaleTop, ScaleWidth, and ScaleHeight, which are expressed in the measurement unit indicated by the ScaleMode property (usually in twips). By default, the ScaleLeft and ScaleTop properties return 0 and refer to the upper left corner of the printable area. The ScaleWidth and ScaleHeight properties return the coordinates of the lower right corner of the printable area.
Visual Basic lets you run other Windows applications using the Shell command, which has this syntax:
TaskId = Shell(PathName, [WindowStyle]) |
PathName can include a command line. WindowStyle is one of the following constants: 0-vbHide (window is hidden and focus is passed to it), 1-vbNormalFocus (window has focus and is restored to its original size and position), 2-vbMinimizedFocus (window is displayed as an icon with focus—this is the default value), 3-vbMaximizedFocus (window is maximized and has the focus), 4-vbNormalNoFocus (window is restored but doesn't have the focus), or 6-vbMinimizedNoFocus (window is minimized and the focus doesn't leave the active window). See, for example, how you can run Notepad and load a file in it:
' No need to provide a path if Notepad.Exe is on the system path. Shell "notepad c:\bootlog.txt", vbNormalFocus |
The Shell function runs the external program asynchronously. This means that the control immediately returns to your Visual Basic application, which can therefore continue to execute its own code. In most cases, this behavior is OK because it takes advantage of the multitasking nature of Windows. But at times you might need to wait for a shelled program to complete (for example, if you need to process its results) or simply to check whether it's still running. Visual Basic doesn't give you a native function to obtain this information, but you can use a few Windows API calls to do the job. I've prepared a multipurpose function that checks whether the shelled program is still executing, waits for the optional timeout you specified (omit the argument to wait forever), and then returns True if the program is still running:
' API declarations Private Declare Function WaitForSingleObject Lib "kernel32" _ (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long Private Declare Function OpenProcess Lib "kernel32" (ByVal dwAccess As _ Long, ByVal fInherit As Integer, ByVal hObject As Long) As Long Private Declare Function CloseHandle Lib "kernel32" _ (ByVal hObject As Long) As Long ' Wait for a number of milliseconds, and return the running status of a ' process. If argument is omitted, wait until the process terminates. Function WaitForProcess(taskId As Long, Optional msecs As Long = -1) _ As Boolean Dim procHandle As Long ' Get the process handle. procHandle = OpenProcess(&H100000, True, taskId) ' Check for its signaled status; return to caller. WaitForProcess = WaitForSingleObject(procHandle, msecs) <> -1 ' Close the handle. CloseHandle procHandle End Function |
The argument passed to this routine is the return value of the Shell function:
' Run Notepad, and wait until it is closed. WaitForProcess Shell("notepad c:\bootlog.txt", vbNormalFocus) |
You have several ways to interact with a running program. In Chapter 16, I show how you can control an application through COM, but not all the external applications can be controlled in this way. And even if they could, sometimes the results aren't worth the additional effort. In less demanding situations, you can get the job done using a simpler approach based on the AppActivate and SendKeys commands. The AppActivate command moves the input focus to the application that matches its first argument:
AppActivate WindowTitle [,wait] |
WindowTitle can be either a string or the return value of a Shell function; in the former case, Visual Basic compares the value with the titles of all the active windows in the system. If there isn't an exact match, Visual Basic repeats the search looking for a window whose title begins with the string passed as an argument. When you pass the taskid value returned by a Shell function, there's no second pass because taskid uniquely identifies a running process. If Visual Basic is unable to find the requested window, a run-time error occurs. Wait is an optional argument that indicates whether Visual Basic should wait until the current application has the input focus before passing it to the other program (Wait = True) or whether the command must execute immediately. (Wait = False, the default behavior.)
The SendKeys statement sends one or more keys to the application that currently has the input focus. This statement supports a rather complex syntax, which lets you specify control keys such as Ctrl, Alt, and Shift keys, arrow keys, function keys, and so on. (See the Visual Basic documentation for more information.) This code runs Notepad and then gives it the focus and pastes the current clipboard contents in its window:
TaskId = Shell("Notepad", vbMaximizedFocus) AppActivate TaskId SendKeys "^V" ' ctrl-V |
You now have all you need to run an external program, interact with it, and find out, if you want, when it completes its execution. I've prepared a demonstration program that does this and lets you experiment with a few different settings. (See Figure 5-5.) Its complete source code is on the companion CD.
Figure 5-5. A demonstration program that illustrates how to use Shell, AppActivate, and SendKeys statements.
A successful Windows application should always provide a guide to novice users, typically in the form of a help file. Visual Basic supports two different ways to display such user information, both using the pages of HLP files.
In both cases, you must first create a help file. To do this, you need a word processor capable of generating files in RTF format (such as Microsoft Word) and a help compiler. On the Visual Basic 6 CD-ROM, you can find the Microsoft Help Workshop, shown in Figure 5-6, which lets you assemble all the docs and bitmaps you have prepared and compile them into an HLP file.
Writing a help file is a complex matter, well beyond the scope of this book. You can get information about this topic from the documentation installed with the Microsoft Help Workshop. In my opinion, however, the most effective approach to this issue is to rely on third-party shareware or commercial programs, such as Blue Sky Software's RoboHelp or WexTech's Doc-to-Help, which make the building of a help file a simple and visual process.
Figure 5-6. The Help Workshop utility is on the Visual Basic CD-ROM but must be installed separately.
Once you have generated an HLP file, you can reference it in your Visual Basic application. You do that either at design time by typing the file's name in the General tab of the Project Properties dialog box or at run time by assigning a value to the App.HelpFile property. The latter approach is necessary when you aren't sure about where the help file will be installed. For instance, you can set this path in a directory under the main folder of your application:
' If this file reference is incorrect, Visual Basic raises an error ' when you later try to access this file. App.HelpFile = App.Path & "\Help\MyApplication.Hlp" |
The first way to offer context-sensitive help is based on the F1 key. This type of help uses the HelpContextID property, which is supported by all Visual Basic visible objects, including forms, intrinsic controls, and external ActiveX controls. You can also enter an application-wide help context ID at design time, in the Project Properties dialog box. (The App object doesn't expose an equivalent property at run time, though.)
When the user presses F1, Visual Basic checks whether the HelpContextID property of the control that has the focus has a nonzero value: in this case, it displays the help page associated with that ID. Otherwise, Visual Basic checks whether the parent form has a nonzero HelpContextID property, and in that case displays the corresponding help page. If both the control's and the form's HelpContextID properties are 0, Visual Basic displays the page that corresponds to the project's help context ID.
Visual Basic also supports an additional way of displaying help, the so-called What's This help. You can add support for this help mode by showing the What's This button at the upper right of a form, as you can see in Figure 5-7. When the user clicks on this button, the mouse cursor changes into an arrow and a question mark, and the user can then click on any control on the form to get a quick explanation of what that control is and does.
Figure 5-7. A zoomed screenshot of the upper right corner of a form whose What's This button has just been clicked.
To take advantage of this feature in your programs, you must set the form's WhatsThisButton property to True, which makes the What's This button appear on the form caption. This property is read-only at run time, so you can set it only at design time in the Properties window. Moreover, to get the What's This button to appear, you must either set the BorderStyle property to 1-Fixed Single or to 3-Fixed Dialog, or you must set the properties MaxButton and MinButton to False.
If you can't meet these requirements, you can't display the What's This button. But you can always provide users with a button or a menu command that enters this mode by executing the form's WhatsThisMode method:
Private Sub cmdWhatsThis_Click() ' Enter What's This mode and change mouse cursor shape. WhatsThisMode End Sub |
Each control on the form (but not the form itself) exposes the WhatsThisHelpID property. You assign this property the help context ID of the page that will be displayed when the user clicks on the control while in What's This mode.
Finally the form's WhatsThisHelp property must be set to True to activate the What's This help. If this property is set to False, Visual Basic reverts to the standard help mechanism based on the F1 key and the HelpContextID property. The WhatsThisHelp property can be set only at design time. At this point, you have three different ways to display a What's This? help topic:
Whatever approach you follow, don't forget that you have to prepare a help page for each control on each form of your application. It's legal to have multiple controls share the same help page, but this arrangement can be quite confusing to the user. Therefore, you typically associate a distinct page with each control.
In these first five chapters, I've shown you how to get the maximum out of the Visual Basic environment and the VBA language. By now, you have enough information to write nontrivial programs. The focus of this book, however, is on object-oriented programming, and in the next two chapters I hope to convince you how much you need OOP to build complex, real-world applications.